#!/usr/bin/env python3

# Copyright (c) 2020-2021 Fpemud <fpemud@sina.com>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.

import os
import platform
import subprocess
import wineprefix
import strict_hwcfg
from .._util import Util
from .._game import MatchContext
from .._game_straits import GameStraitForWindows, GameStraitNeedJoysticks
from .._runner import RunnerBase
from .._runner_straits import RunnerStraitNeedCacheDir


class Wine(RunnerBase):

    class Registry(GameStraitForWindows.Registry):

        def __init__(self, altRegObj):
            self._altRegObj = altRegObj

        def has_key(self, key):
            return self._altRegObj.has_key(key)

        def has_value(self, key, value_name, value_type=None):
            return self._altRegObj.has_value(key, value_name, value_type)

        def list_subkeys(self, key, recursive=False):
            return self._altRegObj.list_subkeys(key, recursive)

        def list_values(self, key):
            return self._altRegObj.list_values(key)

        def add_key(self, key):
            self._altRegObj.add_key(key)

        def delete_key(self, key):
            self._altRegObj.delete_key(key)

        def get_value(self, key, value_name, value_type=None, fallback_value_type=None, fallback_value=None):
            return self._altRegObj.get_value(key, value_name, value_type, fallback_value_type, fallback_value_type, fallback_value)

        def add_value(self, key, value_name, value_type, data):
            self._altRegObj.add_value(key, value_name, value_type, data)

        def set_value(self, key, value_name, value_type, data):
            self._altRegObj.set_value(key, value_name, value_type, data)

        def delete_value(self, key, value_name):
            self._altRegObj.delete_value(key, value_name)

        def import_file(self, path, format):
            self._altRegObj.import_file(path, format)

        def save(self):
            self._altRegObj.save()

    def __init__(self, verbose=0):
        super().__init__()
        self._verbose = verbose

    def _initialize(self, sys_info, game_id, game_obj):
        super()._initialize(sys_info, game_id, game_obj)

        self._gameObj = game_obj
        if True:
            assert self._gameObj.has_strait(GameStraitForWindows)
        if True:
            assert hasattr(self._gameObj.spec, "windows_archs")      # means:
            assert hasattr(self._gameObj.spec, "windows_versions")   # means:
            if hasattr(self._gameObj.spec, "languages"):
                # FIXME
                pass
            if hasattr(self._gameObj.spec, "default_language"):
                # FIXME
                pass
            if hasattr(self._gameObj.spec, "resolutions"):
                # FIXMEget_joystick_key_mapping_filepath
                pass
            if hasattr(self._gameObj.spec, "can_fullscreen"):
                # FIXME
                pass
            if hasattr(self._gameObj.spec, "can_windowed"):
                # FIXME
                pass
        if True:
            # FIXME: replace self._gameObj.spec, force can_windowed
            pass
        if True:
            # replace self._gameObj.apply_resolution()
            self._gameObjApplyResolutionFunc = self._gameObj.apply_resolution
            self._gameObj.apply_resolution = self._applyResolution
        if True:
            # replace self._gameObj.apply_fullscreen_or_windowed()
            self._gameObjApplyFullscreenOrWindowsedFunc = self._gameObj.apply_fullscreen_or_windowed
            self._gameObj.apply_fullscreen_or_windowed = self._applyFullscreenOrWindowed

        self._addStrait(RunnerStraitNeedCacheDir(game_id))

    def _prepare(self, context):
        self._context = context
        self._batchCall("_prepare", self._context)

        # prepare wineArch, wineWinVer and self._winePrefixObj
        wineArchObj = None
        wineWinVerObj = None
        self._winePrefixObj = None
        if True:
            wineVerStrList = [x.value for x in Util.expandList(list(GameStraitForWindows.WindowsVersion), self._gameObj.spec.windows_versions, "*", GameStraitForWindows.WindowsVersion)]

            cpuArch = platform.machine()
            if cpuArch == "x86_64":
                if GameStraitForWindows.WindowsArch.WIN64 in self._gameObj.spec.windows_archs:
                    wineArchObj = GameStraitForWindows.WindowsArch.WIN64
                    wineWinVerObj = GameStraitForWindows.WindowsVersion(wineVerStrList[0])
                elif GameStraitForWindows.WindowsArch.WIN32 in self._gameObj.spec.windows_archs:
                    ret = wineprefix.util.filter_windows_versions_by_arch(GameStraitForWindows.WindowsArch.WIN64.value, wineVerStrList)
                    if len(ret) > 0:
                        wineArchObj = GameStraitForWindows.WindowsArch.WIN64
                        wineWinVerObj = GameStraitForWindows.WindowsVersion(ret[0])
                    else:
                        wineArchObj = GameStraitForWindows.WindowsArch.WIN32
                        wineWinVerObj = GameStraitForWindows.WindowsVersion(wineVerStrList[0])
                else:
                    assert False
            elif cpuArch == "i386":
                assert GameStraitForWindows.WindowsArch.WIN32 in self._gameObj.spec.windows_archs
                wineArchObj = GameStraitForWindows.WindowsArch.WIN32
                wineWinVerObj = GameStraitForWindows.WindowsVersion(wineVerStrList[0])
            else:
                assert False

            self._winePrefixObj = wineprefix.WinePrefix(self._context.cache_dirpath, wineArchObj.value, singleton=True, verbose=self._verbose)
            self._winePrefixObj.get_winecfg().set_windows_version(wineWinVerObj.value)

        # prepare self._context
        self._context.windows_arch = wineArchObj
        self._context.windows_version = wineWinVerObj
        self._context.drive_c_dirpath = os.path.join(self._context.cache_dirpath, "drive_c")
        self._context.registry = self.Registry(self._winePrefixObj.get_alternative_reg())

        # prepare joystick data in self._context
        if self._gameObj.has_strait(GameStraitNeedJoysticks):
            self._context.joysticks = Util.hwcfgGeJoystickComponent(strict_hwcfg.probe())

    def _processGameHint(self, hint):
        self._hint = hint

    async def _run(self, item):
        self.__applyQuirkForGameObj()

        runInfo = self._gameObj._getRunInfo(item)
        self.__applyQuirkForWineRunner(runInfo)

        # make sure all the modifications are saved
        self._gameObj._commit()
        self._context.registry.save()

        if runInfo.cwd is not None:
            assert runInfo.cwd.startswith("C:\\")
            cwd = runInfo.cwd[len("C:\\"):].replace("\\", "/")
            cwd = os.path.join(self._context.drive_c_dirpath, cwd)
        else:
            cwd = self._context.drive_c_dirpath

        dllOverrides = {}
        for p in runInfo.custom_dlls:
            assert "\\" not in p and "/" not in p and p.endswith(".dll")
            dllOverrides[p.replace(".dll", "")] = "n"

        envDict = self._winePrefixObj.prepare_envs(dllOverrides)
        envDict.update(runInfo.extra_environemnt_variables)

        cpuCores = None
        if hasattr(self._gameObj.spec, "max_cpu_cores"):
            cpuCores = self._gameObj.spec.max_cpu_cores

        try:
            await self._winePrefixObj.async_run(runInfo.command, cwd=cwd, env=envDict, cpu_cores=cpuCores)
        except subprocess.CalledProcessError:
            if self.__applyQuirkForIgnoreRunError(runInfo):
                pass
            else:
                raise

    def _cleanup(self, on_error=False):
        if not on_error:
            # make sure all the modifications (maybe after game run) are saved
            # self._context.registry must exist when (not on_error)
            self._context.registry.save()

        if hasattr(self, "_winePrefixObj"):
            self._winePrefixObj.dispose()
            del self._winePrefixObj

        if hasattr(self, "_context"):
            self._batchCall("_cleanup", self._context, on_error=on_error)
            del self._context

    @MatchContext.run
    def _applyFullscreenOrWindowed(self, value):
        if value:
            if self._gameObj.spec.can_fullscreen:
                self._gameObjApplyFullscreenOrWindowsedFunc(True)
                self._winePrefixObj.get_winecfg().set_virtual_desktop(False)
            else:
                # FIXME
                assert False
        else:
            if self._gameObj.spec.can_windowed:
                self._gameObjApplyFullscreenOrWindowsedFunc(False)
                self._winePrefixObj.get_winecfg().set_virtual_desktop(False)
            else:
                self._gameObjApplyFullscreenOrWindowsedFunc(True)
                self._winePrefixObj.get_winecfg().set_virtual_desktop(True, self._gameObj.spec.resolutions[0])

    @MatchContext.run
    def _applyResolution(self, value):
        self._gameObjApplyResolutionFunc(value)                                     # set game resolution
        self._winePrefixObj.get_winecfg().set_virtual_desktop(True, value)          # set wine virtual desktop resolution if neccessary

    def __applyQuirkForGameObj(self):
        self._quirkClass = self.__findClass("Quirk" + self._gameObj.__class__.__name__)        # self._quirkClass is also used by self.__applyQuirkForWineRunner()
        if self._quirkClass is not None and hasattr(self._quirkClass, "forGameObj"):
            self._quirkClass.forGameObj(self._context, self._hint, self._gameObj)

    def __applyQuirkForWineRunner(self, runInfo):
        if self._quirkClass is not None and hasattr(self._quirkClass, "forWineRunner"):
            self._quirkClass.forWineRunner(self._context, self._hint, runInfo, self)

    def __applyQuirkForIgnoreRunError(self, runInfo):
        if self._quirkClass is not None and hasattr(self._quirkClass, "forIgnoreRunError"):
            return self._quirkClass.forIgnoreRunError(self._context, self._hint, runInfo)
        else:
            return False

    @staticmethod
    def __findClass(name):
        for obj_name, obj in globals().items():
            if not isinstance(obj, type):
                continue
            if obj_name != name:
                continue
            return obj
        return None


class QuirkBeachHead2002:

    @staticmethod
    def forWineRunner(context, hint, runInfo, runnerObj):
        # This game has 2 processes: menu process and game process
        # menu process crashes when switching to game process, which does not affect anything but shows the crash dialog
        runnerObj._winePrefixObj.get_winetricks().enable_autostart_winedbg(False)


class QuirkLimbo:

    @staticmethod
    def forIgnoreRunError(context, hint, runInfo):
        # This game crashes when closing
        return True


class QuirkPlantsVsZombies:

    @staticmethod
    def forIgnoreRunError(context, hint, runInfo):
        # This game crashes when closing
        return True
